
#ifdef WIN32
#include <Windows.h>
#endif

#if defined(__APPLE__) || defined(MACOSX)
#include <GLUT/glut.h>
#else
#include <gl/glut.h>
#endif

#include <iostream>
#include <math.h>
#include <assert.h>
#include "Vec.h"
#include "trackball.h"
#include "mesh.h"

using namespace std;

Mesh mesh;

unsigned int width = 800, height = 800;
double backgroundColor[]={0.5,0.5,0.5};

Vec lightPos, lightColor, camPos;
std::vector<Vec> colors;
std::vector<double> normals;
std::vector<double> curvatures;
std::vector<double> torsions;
std::vector<double> curvatureDerivatives;

unsigned int mode=0;
unsigned int nbModes = 3;

bool mask = false;
bool toon = false;
bool filtering = true;
bool contours = false;
bool showCurvature = false;
bool showPrincipalHighlights = false;
bool showSuggestiveHighlights = false;

double precision=0;

Vec computeColor(Vertex &v)
{
	Vec vertexToCam = camPos;
	vertexToCam -= v.p;
	vertexToCam.normalize();
	
	if(showCurvature)
	{
		double curvature = v.getCurvature(vertexToCam);
		
		if(curvature <= 0)
		{
			return(Vec(0,0,-curvature/10));
		}
		return(Vec(curvature/10,0,0));
	}
	
	if(!mask)
	{
		Vec lightToVertex = lightPos;
		lightToVertex -= v.p;
		lightToVertex.normalize();
		
		double i = lightToVertex * v.n * 0.8;
		i = (i*i+2)/3;
		
		if(toon)
		{
			if(i>0.875)
			{
				return(Vec(1,1,1)); 
			}
			int k=10;
			double i_prime = floor(k*i)/k;
			return(Vec(i_prime,i_prime,i_prime));
		}
		
		return(Vec(i,i,i));
	}
	
	if(showSuggestiveHighlights || showPrincipalHighlights)
	{
		return(Vec(0.7,0.7,0.7));
	}
	
	return(Vec(1,1,1));
}

double computeNormals(Vertex &v)
{
	Vec vertexToCam = camPos;
	vertexToCam -= v.p;
	vertexToCam.normalize();
	
	return(vertexToCam * v.n);
}

double computeCurvatures(Vertex &v)
{
	Vec vertexToCam = camPos;
	vertexToCam -= v.p;
	vertexToCam.normalize();
	
	return(v.getCurvature(vertexToCam));
}

double computeTorsions(Vertex &v)
{
	Vec vertexToCam = camPos;
	vertexToCam -= v.p;
	vertexToCam.normalize();
	
	return(v.getTorsion(vertexToCam));
}

double computeCurvatureDerivatives(Vertex &v)
{
	Vec vertexToCam = camPos;
	vertexToCam -= v.p;
	vertexToCam.normalize();
	
	return(v.getCurvatureDerivative(vertexToCam));
}

void computeColors()
{
	std::vector<Vec> *result=&colors;
	std::vector<double> *norm=&normals;
	std::vector<double> *curv=&curvatures;
	std::vector<double> *tors=&torsions;
	//std::vector<double> *deriv=&curvatureDerivatives;
	
	for (unsigned int i=0; i<mesh.vertices.size();++i)
	{
		(*result)[i] = computeColor(mesh.vertices[i]);
		(*norm)[i] = computeNormals(mesh.vertices[i]);
		(*curv)[i] = computeCurvatures(mesh.vertices[i]);
		(*tors)[i] = computeTorsions(mesh.vertices[i]);
		//(*deriv)[i] = computeCurvatureDerivatives(mesh.vertices[i]);
	}
	
	
	if(contours)
	{
		glColor3f(0, 0, 0);
		glLineWidth(2);
		
		for(unsigned int i=0; i<mesh.triangles.size(); i++)
		{
			int a = mesh.triangles[i].v[0];
			int b = mesh.triangles[i].v[1];
			int c = mesh.triangles[i].v[2];
			
			if( (normals[a]*normals[b] <= 0)
			   || (normals[b]*normals[c] <= 0)
			   || (normals[a]*normals[c] <= 0) )
			{
				if(normals[a]*normals[b] > 0)
				{
					int tmp = a;
					a = c;
					c = b;
					b = tmp;
				}
				else if (normals[a]*normals[c] > 0)
				{
					int tmp = a;
					a = b;
					b = c;
					c = tmp;
				}
				
				Vec pa = mesh.vertices[a].p;
				Vec pb = mesh.vertices[b].p;
				Vec pc = mesh.vertices[c].p;
				
				Vec p1 = pa + ((pc-pa)*normals[a])/(normals[a] - normals[c]);
				Vec p2 = pa + ((pb-pa)*normals[a])/(normals[a] - normals[b]);
				
				glBegin(GL_LINES);
				glVertex3f(p1.x, p1.y, p1.z);
				glVertex3f(p2.x, p2.y, p2.z);
				glEnd();
			}
		}
	}
	
	if(showPrincipalHighlights)
	{
		for(unsigned int i=0; i<mesh.triangles.size(); i++)
		{
			int a = mesh.triangles[i].v[0];
			int b = mesh.triangles[i].v[1];
			int c = mesh.triangles[i].v[2];
			
			Vec pa = mesh.vertices[a].p;
			Vec pb = mesh.vertices[b].p;
			Vec pc = mesh.vertices[c].p;
			
			if( (torsions[a]*torsions[b] <= 0)
			   || (torsions[b]*torsions[c] <= 0))
			{
				if(torsions[a]*torsions[b] > 0)
				{
					int tmp = a;
					a = c;
					c = b;
					b = tmp;
				}
				else if (torsions[a]*torsions[c] > 0)
				{
					int tmp = a;
					a = b;
					b = c;
					c = tmp;
				}
				
				pa = mesh.vertices[a].p;
				pb = mesh.vertices[b].p;
				pc = mesh.vertices[c].p;
				
				Vec p1 = pa + ((pc-pa)*torsions[a])/(torsions[a] - torsions[c]);
				Vec p2 = pa + ((pb-pa)*torsions[a])/(torsions[a] - torsions[b]);
				
				if(filtering)
				{
					double theta1 = mesh.vertices[a].theta + ((mesh.vertices[c].theta-mesh.vertices[a].theta)*curvatures[a])/(curvatures[a] - curvatures[c]);
					double theta2 = mesh.vertices[a].theta + ((mesh.vertices[b].theta-mesh.vertices[a].theta)*curvatures[a])/(curvatures[a] - curvatures[b]);
					
					double theta = max(theta1, theta2);
					theta = max(theta, -theta);
					
					if(theta < 0.25)
					{
						continue;
					}
				}
				
				glColor3f(1, 1, 1);
				glLineWidth(2);
				
				glBegin(GL_LINES);
				glVertex3f(p1.x, p1.y, p1.z);
				glVertex3f(p2.x, p2.y, p2.z);
				glEnd();
			}
		}
	}
	
	if(mode >= 1)
	{
		for(unsigned int i=0; i<mesh.triangles.size(); i++)
		{
			int a = mesh.triangles[i].v[0];
			int b = mesh.triangles[i].v[1];
			int c = mesh.triangles[i].v[2];
			
			Vec pa = mesh.vertices[a].p;
			Vec pb = mesh.vertices[b].p;
			Vec pc = mesh.vertices[c].p;
			
			if( (curvatures[a]*curvatures[b] <= 0)
			   || (curvatures[b]*curvatures[c] <= 0))
			{
				if(curvatures[a]*curvatures[b] > 0)
				{
					int tmp = a;
					a = c;
					c = b;
					b = tmp;
				}
				else if (curvatures[a]*curvatures[c] > 0)
				{
					int tmp = a;
					a = b;
					b = c;
					c = tmp;
				}
				
				pa = mesh.vertices[a].p;
				pb = mesh.vertices[b].p;
				pc = mesh.vertices[c].p;
				
				Vec p1 = pa + ((pc-pa)*curvatures[a])/(curvatures[a] - curvatures[c]);
				Vec p2 = pa + ((pb-pa)*curvatures[a])/(curvatures[a] - curvatures[b]);
				
				double da = (pa - camPos).length();
				double db = (pb - camPos).length();
				
				if(filtering)
				{
					double theta1 = mesh.vertices[a].theta + ((mesh.vertices[c].theta-mesh.vertices[a].theta)*curvatures[a])/(curvatures[a] - curvatures[c]);
					double theta2 = mesh.vertices[a].theta + ((mesh.vertices[b].theta-mesh.vertices[a].theta)*curvatures[a])/(curvatures[a] - curvatures[b]);
					
					double theta = max(theta1, theta2);
					theta = max(theta, -theta);
					
					if(theta < 0.25)
					{
						continue;
					}
				}
				
				if( mode == 1
					|| ((db-da > 0) && ( (curvatures[a] - curvatures[b])/(pb.distanceTo(pa)) > precision ))
					|| ((db-da < 0) && ( (curvatures[b] - curvatures[a])/(pb.distanceTo(pa)) > precision )) )
				{
					glColor3f(0, 0, 0);
					glLineWidth(2);
					
					glBegin(GL_LINES);
					glVertex3f(p1.x, p1.y, p1.z);
					glVertex3f(p2.x, p2.y, p2.z);
					glEnd();
				}
				else if( showSuggestiveHighlights )
				{
					glColor3f(1, 1, 1);
					glLineWidth(2);
					
					glBegin(GL_LINES);
					glVertex3f(p1.x, p1.y, p1.z);
					glVertex3f(p2.x, p2.y, p2.z);
					glEnd();
				}
			}
		}
	}
}

void draw()
{
	computeColors();
	mesh.drawWithColors(colors);
}

void idle()
{
	camPos=getCameraPosition();
	
	glutPostRedisplay();
}


void changeLight(const Vec &selectedPos, const Vec &selectedNormal, Vec origin, Vec direction)
{
	double r = 1.5;
	
	double a = direction.squaredLength();
	double b = 2 * origin * direction;
	double c = origin.squaredLength() - r*r;
	
	double delta = b*b - 4*a*c;
	
	if(delta >= 0)
	{
		delta = sqrt(delta);
		double l1 = -(b-delta)/(2*a), l2 = -(b+delta)/(2*a);
		
		Vec pos1 = origin + l1*direction, pos2 = origin + l2*direction;
		double d1 = (pos1 - camPos).squaredLength();
		double d2 = (pos2 - camPos).squaredLength();
		
		if(d1*d1 < d2*d2)
		{
			lightPos = pos1;
		}
		else
		{
			lightPos = pos2;
		}
	}
}

void dealWithUserInput(int x, int y)
{
	Vec worldPoint=getWorldPositionOfPixel(x, height-y);
	int selectedVertex=mesh.getClosestVertexIndex(camPos, worldPoint-camPos);
	if (selectedVertex>=0)
	{
		Vec selectedPos = mesh.vertices[selectedVertex].p;
		Vec selectedNormal = mesh.vertices[selectedVertex].n;
		Vec direction = (worldPoint - camPos);
		direction.normalize();
		changeLight(selectedPos, selectedNormal, camPos, direction);
	}
}

void keyboard(unsigned char key, int x, int y)
{
	switch (key)
    {
		case '+':
		{
			if(precision == 0)
			{
				precision = 1;
			}
			precision *= 1.5;
			cout<<"Precision = "<<precision<<endl;
			break;
		}
		case '-':
		{
			precision /= 1.5;
			if(precision <= 1)
			{
				precision = 0;
			}
			cout<<"Precision = "<<precision<<endl;
			break;
		}
		case 'a':
		{
			mask = true;
			toon = false;
			contours = true;
			filtering = true;
			showCurvature = false;
			mode = 2;
			precision = 80;
			cout<<"Showing best results!"<<endl;
			break;
		}
		case 'A':
		{
			mask = false;
			toon = true;
			contours = true;
			filtering = true;
			showCurvature = false;
			mode = 2;
			precision = 80;
			cout<<"Showing best results!"<<endl;
			break;
		}
		case 'i':
		{
			mask = false;
			toon = false;
			contours = false;
			filtering = false;
			showCurvature = false;
			mode = 0;
			precision = 0;
			cout<<"Reinitialized."<<endl;
			break;
		}
		case 'c':
		{
			contours = !contours;
			cout<<"showContours = "<<contours<<endl;
			break;
		}
		case 'C':
		{
			showCurvature = !showCurvature;
			cout<<"showCurvature = "<<showCurvature<<endl;
			break;
		}
		case 'f':
		{
			filtering = !filtering;
			cout<<"filtering = "<<filtering<<endl;
			break;
		}
		case 'h':
		{
			showPrincipalHighlights = !showPrincipalHighlights;
			cout<<"showPrincipalHighlights = "<<showPrincipalHighlights<<endl;
			break;
		}
		case 'H':
		{
			showSuggestiveHighlights = !showSuggestiveHighlights;
			cout<<"showSuggestiveHighlights = "<<showSuggestiveHighlights<<endl;
			break;
		}
		case 'm':
		{
			mode = (mode + 1)%nbModes;
			cout<<"Mode = "<<mode<<endl;
			break;
		}
		case 'M':
		{
			mode = (mode + nbModes-1)%nbModes;
			cout<<"Mode = "<<mode<<endl;
			break;
		}
		case 's':
		{
			mask = !mask;
			cout<<"mask = "<<mask<<endl;
			break;
		}
		case 't':
		{
			toon = !toon;
			cout<<"toon = "<<toon<<endl;
			break;
		}
		case 'q':
		{
			exit(EXIT_SUCCESS);
			break;
		}
		case ' ':
		{
			dealWithUserInput(x,y);
			break;
		}
		default:
		{
			break;
		}
	}
}

void display(void)
{
    //We clear everything and get back to the original position
	glClear( GL_COLOR_BUFFER_BIT  | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	
	//Now we take the trackball into account and draw everything
    tbVisuTransform();
    draw( );	
    glutSwapBuffers();
}

void reshape(int w, int h)
{
    glViewport(0, 0, (GLsizei) w, (GLsizei) h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective (50, (double)w/h, 1, 10);
    glMatrixMode(GL_MODELVIEW);
}

void init(const char * fileName)
{	
    mesh.loadMesh(fileName);
	colors.resize(mesh.vertices.size());
	normals.resize(mesh.vertices.size());
	curvatures.resize(mesh.vertices.size());
	torsions.resize(mesh.vertices.size());
	curvatureDerivatives.resize(mesh.vertices.size());
	lightPos = Vec(0,0,3);
	lightColor = Vec(1,1,1);
	computeColors();
}

int main(int argc, char** argv)
{
    glutInit (&argc, argv);
	
    init(argc == 2 ? argv[1] : "DavidHeadCleanMax.obj");
	
    //Buffer
	glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH );
	
    //Window
    glutInitWindowPosition(200, 100);
    glutInitWindowSize(width,height);
    glutCreateWindow(argv[0]);
	
    //Point of view
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glTranslatef(0,0,-4);
	
	//TrackBall
    tbInitTransform();
    tbHelp();
	
    glDisable( GL_LIGHTING );
    glEnable(GL_COLOR_MATERIAL);
    glEnable(GL_NORMALIZE);
    
    //GLUT Callbacks
    glutReshapeFunc(reshape);
    glutKeyboardFunc(keyboard);
    glutDisplayFunc(display);
    glutMouseFunc(tbMouseFunc);
    glutMotionFunc(tbMotionFunc);
    glutIdleFunc(idle);
	
	
    //Drawing mode
    glEnable( GL_DEPTH_TEST );
    glShadeModel(GL_SMOOTH);
	
    //Clear everything before painting
    glClearColor (backgroundColor[0],backgroundColor[1], backgroundColor[2], 0.0);
    glClear( GL_COLOR_BUFFER_BIT  | GL_DEPTH_BUFFER_BIT);
	
	//Here we go!
    glutMainLoop();
	
    return 0;
}


